
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: XMLExporter.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar.xml;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Set;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.dellroad.sidekar.Database;
import org.dellroad.sidekar.Item;
import org.dellroad.sidekar.schema.model.SchemaModel;
import org.dellroad.sidekar.util.SidekarException;
import org.jibx.runtime.BindingDirectory;
import org.jibx.runtime.IBindingFactory;
import org.jibx.runtime.IMarshallingContext;
import org.jibx.runtime.JiBXException;

/**
 * Exports a database to XML.
 */
public final class XMLExporter {

    private static final String BINDING_NAME = "database";

    private XMLExporter() {
    }

    /**
     * Export a database to the given output in XML format.
     *
     * <p>
     * If successful, {@code out} will be closed. UTF-8 encoding is used for the generated XML document.
     * </p>
     *
     * @param out XML output stream; will be closed if successful
     * @param db {@link Database} to export; must be already open
     * @param nodata if true, no data is included in the export, only the schema information
     * @throws SidekarException if an underlying database error occurs
     */
    public static void exportDatabase(final OutputStream out, final Database db, final boolean nodata) {
        db.performAction(new VoidDatabaseAction() {
            public void executeVoid() {
                export(out, db, new XMLDatabase(db, nodata));
            }
        });
    }

    /**
     * Export an {@link Item} to the given output in XML format.
     *
     * <p>
     * The fields to be output may be specified in {@code fieldNames}; if {@code null},
     * all fields will be included.
     *
     * <p>
     * If successful, {@code out} will be closed. UTF-8 encoding is used for the generated XML document.
     * </p>
     *
     * @param out XML output stream; will be closed if successful
     * @param item item to export
     * @param fieldNames if non-null, only fields whose names are in this set will be included
     * @throws NullPointerException if {@code item} is {@code null}
     * @throws SidekarException if the object is not a supported object
     * @throws SidekarException if an underlying database error occurs
     */
    public static void exportItem(OutputStream out, Item<?> item, Set<String> fieldNames) {
        export(out, item.getSidekarEntity().getDatabase(), new XMLItem(item, fieldNames));
    }

    /**
     * Export an {@link Item} to the given output in XML format.
     *
     * <p>
     * This is a convenience method, equivalent to:
     * <blockquote>{@code exportItem(out, item, null)}</blockquote>
     * </p>
     *
     * <p>
     * If successful, {@code out} will be closed. UTF-8 encoding is used for the generated XML document.
     * </p>
     *
     * @param out XML output stream; will be closed if successful
     * @param item item to export
     * @throws NullPointerException if {@code obj} is {@code null}
     * @throws SidekarException if the object is not a supported object
     * @throws SidekarException if an underlying database error occurs
     */
    public static void exportItem(OutputStream out, Item<?> item) {
        exportItem(out, item, null);
    }

    /**
     * Export multiple {@link Item}s to the given output in XML format.
     *
     * <p>
     * If successful, {@code out} will be closed. UTF-8 encoding is used for the generated XML document.
     * </p>
     *
     * @param out XML output stream; will be closed if successful
     * @param items {@link Item} iterator producing the database items to export
     * @throws NullPointerException if {@code obj} is {@code null}
     * @throws SidekarException if the object is not a supported object
     * @throws SidekarException if an underlying database error occurs
     */
    public static void exportItems(OutputStream out, Iterator<?> items) {
        export(out, null, new XMLData(items));
    }

    /**
     * Export a database schema to the given output in XML format.
     *
     * <p>
     * If successful, {@code out} will be closed. UTF-8 encoding is used for the generated XML document.
     * </p>
     *
     * @param out XML output stream; will be closed if successful
     * @param schema the schema to export
     * @throws NullPointerException if {@code obj} is {@code null}
     * @throws SidekarException if the object is not a supported object
     * @throws SidekarException if an underlying database error occurs
     */
    public static void exportSchema(OutputStream out, SchemaModel schema) {
        export(out, null, schema);
    }

    // Export something
    private static void export(OutputStream out, Database db, final Object obj) {
        final IMarshallingContext ctx = createMarshallingContext(out, obj);
        if (db != null) {
            db.performAction(new VoidDatabaseAction() {
                public void executeVoid() {
                    try {
                        ctx.marshalDocument(obj);
                    } catch (JiBXException e) {
                        throw new SidekarException(e);
                    }
                }
            });
        } else {
            try {
                ctx.marshalDocument(obj);
            } catch (JiBXException e) {
                throw new SidekarException(e);
            }
        }
    }

    // Create the marshalling context
    private static IMarshallingContext createMarshallingContext(OutputStream out, Object obj) {
        try {
            IBindingFactory bindingFactory = BindingDirectory.getFactory(BINDING_NAME, XMLExporter.class.getPackage().getName());
            final IMarshallingContext ctx = bindingFactory.createMarshallingContext();
            ctx.setOutput(out, null);
            ctx.setIndent(2);
            return ctx;
        } catch (JiBXException e) {
            throw new SidekarException(e);
        }
    }

    /**
     * Command line entry point.
     */
    public static void main(final String[] args) {
        MainHelp.run(new ExceptingRunnable() {
            public void run() throws Exception {
                doMain(args);
            }
        });
    }

    private static boolean doMain(String[] args) throws Exception {

        // Parse command line
        String exname = null;
        File directory = null;
        OutputStream output = System.out;
        boolean nodata = false;
        boolean verbose = true;
        int i;
        for (i = 0; i < args.length; i++) {
            if (args[i].charAt(0) != '-' || args[i].equals("--"))
                break;
            else if (args[i].equals("-nodata"))
                nodata = true;
            else if (args[i].equals("-q"))
                verbose = false;
            else
                usageError();
        }
        switch (args.length - i) {
        case 2:
            output = new FileOutputStream(args[i + 2]);
            // FALLTHROUGH
        case 1:
            directory = new File(args[i]);
            break;
        default:
            usageError();
            break;
        }

        // Verify directory
        if (!directory.exists())
            errout("directory `" + directory + "' does not exist");
        else if (!directory.isDirectory())
            errout("directory `" + directory + "' is not a directory");

        // Configure logging
        MainHelp.setupLogging(verbose ? Level.INFO : Level.WARN);
        Logger log = Logger.getLogger(XMLExporter.class);

        // Open database
        log.info("opening database in directory " + directory);
        Database db = new Database();
        db.open();

        // Hack to workaround JiBX closing our output too soon
        IgnoreCloseOutputStream alternateOutput = new IgnoreCloseOutputStream(output);

        // Do export
        log.info("exporting database in directory " + directory);
        XMLExporter.exportDatabase(alternateOutput, db, nodata);
        db.close();

        // JiBX doesn't add trailing newline
        new PrintWriter(alternateOutput, true).println();

        // Close output
        output.close();

        // Done
        log.info("database successfully exported from directory " + directory);
        return true;
    }

    private static void usageError() {
        System.err.println("Usage: java " + XMLExporter.class.getName() + " [-q] [-nodata] directory [ output.xml ]");
        System.err.println("Options:");
        System.err.println("    -nodata\tOutput the schema only (no actual data)");
        System.err.println("    -q\t\tQuiet output");
        System.exit(1);
    }

    private static void errout(String message) {
        System.err.println(XMLExporter.class.getSimpleName() + ": " + message);
        System.exit(1);
    }

    // Hack to prevent JiBX from closing the output before we want it to
    static class IgnoreCloseOutputStream extends FilterOutputStream {

        IgnoreCloseOutputStream(OutputStream out) {
            super(out);
        }

        @Override
        public void close() throws IOException {
            // ignore
        }
    }
}

